/*
 * Copyright (c) 2016-2021 Marco Cawthorne <marco@icculus.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/** @brief Get the absolute center pos of a entity */
vector getEntityCenterPos(entity e) {
	vector newVec;
	newVec[0] = e.absmin[0] + (0.5 * (e.absmax[0] - e.absmin[0]));
	newVec[1] = e.absmin[1] + (0.5 * (e.absmax[1] - e.absmin[1]));
	newVec[2] = e.absmin[2] + (0.5 * (e.absmax[2] - e.absmin[2]));
	return newVec;
}

class csbot:NSBot
{
	void(void) csbot;

	/* some objectives */
	virtual void(void) RunToConfront;
	virtual void(void) RunToBomb;
	virtual void(int)  RunToBombsite;
	virtual void(void) RunToRandomBombsite;
	virtual void(int)  RunToEscapeZone;
	virtual void(void) RunToRandomEscapeZone;
	virtual void(int)  RunToVIPSafetyZone;
	virtual void(void) RunToRandomVIPSafetyZone;
	virtual void(void) RunToHostages;
	virtual void(vector, int) Roam;

	virtual void(void) CreateObjective;
	virtual void(void) PostFrame;
	virtual void(void) WeaponThink;

	/* helpers */
	virtual entity(string, int) GetEntityByNameAndIndex;
	virtual entity(int) GetBombsiteByIndex;
	virtual entity(int) GetEscapeZoneByIndex;
	virtual entity(int) GetVIPSafetyZoneByIndex;
	virtual void(vector, float) AimLerp;

	int m_actionIsPlanting;
	int m_actionIsDefusing;

	/* Workaround:
	* gflags is not yet set when CSBot_BuyStart_Shop() or CreateObjective()
	* are called, so we back it up on PostFrame() and use that instead.
	* Known issues it solves:
	* - Check if the bot is in a Bomb Zone (gflags & GF_BOMBZONE)
	* - Check if the bot is in a Buy Zone (gflags & GF_BUYZONE) */
	int m_gflagsBackup;
};

void
csbot::RunToConfront(void)
{
	entity t;
	if (team == TEAM_T) {
		t = Route_SelectRandom("info_player_start");
	} else {
		t = Route_SelectRandom("info_player_deathmatch");
	}

	ChatSayTeam("Going to run to the Enemy Spawn!");

	if (t)
		RouteToPosition(t.origin);
}
/* go to the planted bomb */
void
csbot::RunToBomb(void)
{
	entity e = world;
	e = find(e, ::model, "models/w_c4.mdl");

	if (e) {
		RouteToPosition(e.origin);
		ChatSayTeam("Going to run to the Bomb!");
	}
}

/* go to given bombsite */
void
csbot::RunToBombsite(int bombsiteIndex)
{
	entity e = GetBombsiteByIndex(bombsiteIndex);
	RouteToPosition(getEntityCenterPos(e));
	ChatSayTeam(strcat("Going to run to Bomb Site ", itos(bombsiteIndex), "!"));
}

/* go to random bombsite */
void
csbot::RunToRandomBombsite(void)
{
	RunToBombsite(random(0, g_cs_bombzones));
}

/* go to given escape zone */
void
csbot::RunToEscapeZone(int index)
{
	entity e = GetEscapeZoneByIndex(index);
	RouteToPosition(getEntityCenterPos(e));
	ChatSayTeam(strcat("Going to run to Escape Zone ", itos(index), "!"));
}

/* go to a random escape zone */
void
csbot::RunToRandomEscapeZone(void)
{
	RunToEscapeZone(random(0, g_cs_escapezones));
}

/* go to given VIP Safety Zone */
void
csbot::RunToVIPSafetyZone(int index)
{
	entity e = GetVIPSafetyZoneByIndex(index);
	RouteToPosition(getEntityCenterPos(e));
	ChatSayTeam(strcat("Going to run to VIP Safety Zone ", itos(index), "!"));
}

/* go to a random VIP Safety Zone */
void
csbot::RunToRandomVIPSafetyZone(void)
{
	RunToVIPSafetyZone(random(0, g_cs_vipzones));
}

void
csbot::RunToHostages(void)
{
	entity e = world;

	e = find(e, ::classname, "hostage_entity");

	RouteToPosition(e.origin);
	ChatSayTeam("Going to run to the hostages!");
}

/** @brief Let the bot roam within a maximum distance from a given origin. */
void csbot::Roam(vector roamOrigin, int maxDistance) {
	/* Get random point whitin a radius from the given origin */
	int angle = random(0, 360); /* random angle. */
	int distance = random(0, maxDistance); /* random distance */
	float radian = angle * 3.145238095238 / 180;
	vector randLoc = roamOrigin;
	randLoc.x += sin(radian) * distance;
	randLoc.y += cos(radian) * distance;

	/* Find closest waypoint to our random location. */
	float flBestDist = COST_INFINITE;
	int iBestNodeIndex = -1;
	for (int i = 0; i < g_iNodes; i++) {
		float fDist = vlen(g_pNodes[i].origin - randLoc) - g_pNodes[i].radius;

		if (fDist > (float)maxDistance) {
			/* Distance is greater then our maxDistance */
			continue;
		}

		if (fDist < flBestDist) {
			flBestDist = fDist;
			iBestNodeIndex = i;
		}
	}

	if (iBestNodeIndex == -1) {
		/* TODO No waypoint in range found */
		print("WARNING!: Roaming failed, could not find waypoint in range.\n");
		return;
	}

	/* Go to the random waypoint. */
	RouteToPosition(g_pNodes[iBestNodeIndex].origin);
}

void
csbot::CreateObjective(void)
{
	/* Bomb defuse map */
	if (g_cs_bombzones > 0) {
		/* Bomb is planted */
		if (g_cs_bombplanted) {
			entity eBomb = find(world, ::model, "models/w_c4.mdl");
			if (eBomb == world) {
				/* No bomb model found, but it is/was planted */

				/* RoundOver: Bomb is defused */
				if (g_cs_gamestate == GAME_END) {
					RunToRandomBombsite();
					return;
				}

				/* Error */
				print("WARNING! g_cs_bombplanted == TRUE, but bomb model "
					  "cannot be found in the world.\n");
				return;
			}

			if (team == TEAM_CT) {
				if (g_cs_bombbeingdefused && m_actionIsDefusing == FALSE) {
					/* Bomb is being defused but not by this bot */
					/* Go and roam the defuser */
					Roam(eBomb.origin, 300);
					return;
				}

				if (m_actionIsDefusing) {
					if (!g_cs_bombbeingdefused) {
						/* Defusing complete or somehow failed. */
						m_actionIsDefusing = FALSE;
					} else {
						/* Continue defusing. */
						input_buttons |= (INPUT_BUTTON5 | INPUT_BUTTON8);
						input_movevalues = [0,0,0];
						button5 = input_buttons & INPUT_BUTTON5; // don't release button5
					}
				}
				else {
					int distToBomb = floor(vlen(eBomb.origin - origin));
					if (distToBomb > 60) {
						/* To far away from the bomb to defuse it, run to it! */
						RunToBomb();
					} else {
						/* Aim at the bomb. */
						input_buttons |= INPUT_BUTTON8; // duck
						if ((flags & FL_ONUSABLE)) {
							// Aimed at the bomb, ready to defuse!
							ChatSayTeam("Defusing!");
							input_buttons |= INPUT_BUTTON5;
							input_movevalues = [0,0,0];
							button5 = input_buttons & INPUT_BUTTON5; // don't release button5
							m_actionIsDefusing = TRUE;
						} else {
							// Do the real aiming
							float flLerp = bound(0.0f, frametime * 45, 1.0f);  // aim speed
							AimLerp(eBomb.origin + [0, 0, -6], flLerp);
						}
					}
				}
			}
			/* team == TEAM_T */
			else {
				/* Let T bots roam around the planted bomb */
				Roam(eBomb.origin, 500);
			}
			return;
		}
		/* Bomb is NOT planted */
		else {
			if (team == TEAM_T) {
				/* T-bot: plant bomb */
				if ((g_items & ITEM_C4BOMB)) {
					/* We carry the bomb */
					if (m_gflagsBackup & GF_BOMBZONE) {
						/* We are at a bombsite and ready to plant the bomb */
						if (activeweapon != WEAPON_C4BOMB) {
							activeweapon = WEAPON_C4BOMB;
							Weapons_Draw((player)self);
						}

						if (!m_actionIsPlanting) {
							ChatSayTeam("Going to plant the bomb!");
							m_actionIsPlanting = TRUE;
						}

						/* Workaround */
						gflags = m_gflagsBackup;

						/* Duck and plant bomb. */
						input_buttons = (INPUT_BUTTON0 | INPUT_BUTTON8);
						input_movevalues = [0,0,0];
					}
					else {
						/* Go to a bombsite first */
						RunToRandomBombsite();
					}
					return;
				}
				else {
					/* T-bot: check if the bomb has been dropped */
					entity e = find(world, ::model, "models/w_backpack.mdl");
					if (e != world) {
						/* The bomb backpack has been dropped */
						/* Go fetch dropped bomb! */
						ChatSayTeam("Arrr! Bomb on the ground, going to fetch it!");
						RouteToPosition(getEntityCenterPos(e));
						return;
					}
				}
			}
		}
	}

	if (g_cs_escapezones && team == TEAM_T) {
		RunToRandomEscapeZone();
		return;
	}

	if (random() < 0.5 && g_cs_escapezones > 0 && team == TEAM_CT) {
		RunToRandomEscapeZone();
		return;
	}

	if (g_cs_vipzones > 0 && team == TEAM_CT) {
		RunToRandomVIPSafetyZone();
		return;
	}

	if (random() < 0.5 && g_cs_vipzones > 0 && team == TEAM_T) {
		RunToRandomVIPSafetyZone();
		return;
	}

	if (random() < 0.5) {
		if (g_cs_hostagestotal > 0)
			RunToHostages();
		if (g_cs_bombzones > 0)
			RunToRandomBombsite();
	} else {
		RunToConfront();
	}
}

/** @brief Aim towards a given (vector)aimpos with a given (float)lerp speed.
 *
 * @note
 * Copied code from nuclide botlib (inside bot::RunAI), maybe make this a
 * method there, could be usefull for other stuff?
 **/
void csbot::AimLerp(vector aimpos, float flLerp) {
	vector aimdir, vecNewAngles;

	vector oldAngle = v_angle;

	/* that's the old angle */
	makevectors(v_angle);
	vecNewAngles = v_forward;

	/* aimdir = new final angle */
	aimdir = vectoangles(aimpos - origin);
	makevectors(aimdir);

	/* slowly lerp towards the final angle */
	vecNewAngles[0] = Math_Lerp(vecNewAngles[0], v_forward[0], flLerp);
	vecNewAngles[1] = Math_Lerp(vecNewAngles[1], v_forward[1], flLerp);
	vecNewAngles[2] = Math_Lerp(vecNewAngles[2], v_forward[2], flLerp);

	/* make sure we're aiming tight */
	v_angle = vectoangles(vecNewAngles);
	v_angle[0] = Math_FixDelta(v_angle[0]);
	v_angle[1] = Math_FixDelta(v_angle[1]);
	v_angle[2] = Math_FixDelta(v_angle[2]);
	angles[0] = Math_FixDelta(v_angle[0]);
	angles[1] = Math_FixDelta(v_angle[1]);
	angles[2] = Math_FixDelta(v_angle[2]);
	input_angles = v_angle;
}

void
csbot::PostFrame(void)
{
	if (team == 0) {
		CSEv_JoinAuto();
	}

	team = infokeyf(this, "*team");
	m_gflagsBackup = gflags;
};

void
csbot::WeaponThink(void)
{
	if (activeweapon == WEAPON_KNIFE)
		return;

	/* clip empty */
	if (a_ammo1 == 0) {
		/* still got ammo left, reload! */
		if (a_ammo2 != 0) {
			input_buttons &= ~INPUT_BUTTON0;
			input_buttons |= INPUT_BUTTON4;
		} else {
			Weapons_SwitchBest(this);
		}
	}
};

/** @brief Get entity by class name and index **/
entity
csbot::GetEntityByNameAndIndex(string name, int index)
{
	int curIndex = 0;
	for (entity a = world; (a = find(a, ::classname, name));) {
		if (curIndex == index) {
			return a;
		}
		++curIndex;
	}
	print("WARNING: cstrike/server/bot.qc GetEntityByNameAndIndex: no entity '",
		  name, "' with index ", itos(index), "!\n");
	return world;
}

/** @brief Get bombsite entity by bombsite index
 *
 *  @note
 *  When there are for example 2 bombsites (g_cs_bombzones == 2) then valid
 *  indexes would be 0 and 1.
 * */
entity
csbot::GetBombsiteByIndex(int index)
{
	return GetEntityByNameAndIndex("func_bomb_target", index);
}

/** @brief Get Escape Zone entity by index **/
entity
csbot::GetEscapeZoneByIndex(int index)
{
	return GetEntityByNameAndIndex("func_escapezone", index);
}

/** @brief Get VIP Safety Zone entity by index **/
entity
csbot::GetVIPSafetyZoneByIndex(int index)
{
	return GetEntityByNameAndIndex("func_vip_safetyzone", index);
}

void
csbot::csbot(void)
{
	targetname = "_csbot_";
	team = infokeyf(this, "*team");
	m_actionIsPlanting = FALSE;
	m_actionIsDefusing = FALSE;
	m_gflagsBackup = 0;
}

void
CSBot_BombPlantedNotify(void)
{
	for (entity a = world; (a = find(a, classname, "player"));) {
		if (clienttype(a) != CLIENTTYPE_REAL) {
			csbot targ;
			targ = (csbot)a;

			if (targ.team == TEAM_T)
				continue;
			if (targ.health <= 0)
				continue;

			targ.RunToRandomBombsite();
		}
	}
}

void
CSBot_HostageRescueNotify(void)
{
	for (entity a = world; (a = find(a, classname, "player"));) {
		if (clienttype(a) != CLIENTTYPE_REAL) {
			csbot targ;
			targ = (csbot)a;

			if (targ.team == TEAM_T)
				continue;
			if (targ.health <= 0)
				continue;

			targ.RunToHostages();
		}
	}
}

void
CSBot_BuyStart_Shop(void)
{
	int done = 0;
	int count = 0;
	player pl = (player)self;

	pl.team = infokeyf(pl, "*team");

	/* Workaround */
	pl.gflags = ((csbot)pl).m_gflagsBackup;

	count = 0;
	while (done != 1) {
		int r = floor(random(1,17));

		if (pl.team == TEAM_T) {
			if (r == WEAPON_M4A1) { continue; }
			if (r == WEAPON_AUG) { continue; }
			if (r == WEAPON_SG550) { continue; }
			if (r == WEAPON_FIVESEVEN) { continue; }
			if (r == WEAPON_TMP) { continue; }
		} else if (pl.team == TEAM_CT) {
			if (r == WEAPON_AK47) { continue; }
			if (r == WEAPON_SG552) { continue; }
			if (r == WEAPON_G3SG1) { continue; }
			if (r == WEAPON_ELITES) { continue; }
			if (r == WEAPON_MAC10) { continue; }
		}

		if (g_cstrikeWeaponPrice[r] <= pl.money) {
			CSEv_BuyWeapon_f((float)r);
			done = 1;
		}
		count++;

		/* give it enough attempts */
		if (count > 17)
			done = 1;
	}

	/* CT: Random buy bomb defuse kit when enough money left */
	if (pl.team == TEAM_CT && g_cs_bombzones > 0 &&
		g_cstrikeUtilPrice[(float)5] <= pl.money &&
		random() < 0.5)
	{
		CSEv_BuyEquipment_f((float)5);  // ITEM_DEFUSAL
	}

	/* need armor */
	if (pl.armor < 100) {
		if (pl.money >= g_cstrikeUtilPrice[1]) /* kevlar and helmet */
			CSEv_BuyEquipment_f(1);
		else if (pl.money >= g_cstrikeUtilPrice[0]) /* just kevlar */
			CSEv_BuyEquipment_f(0);
	} else if (!(pl.g_items & ITEM_HELMET)) { /* we need helmet */
		if (pl.money >= 350) /* kevlar and helmet */
			CSEv_BuyEquipment_f(1);
	}

	/* make SURE we switch to it */
	for (int i = 0; i < g_weapons.length; i++)
		if (pl.g_items & g_weapons[i].id) {
			pl.activeweapon = i;
			Weapons_Draw(pl);
			return;
		}

	/* force buy right now */
	CSEv_AmmoBuyPrimary();
	CSEv_AmmoBuySecondary();
}

void
CSBot_BuyStart(void)
{
	for (entity a = world; (a = find(a, classname, "player"));) {
		if (clienttype(a) != CLIENTTYPE_REAL) {
			if (a.health <= 0)
				continue;

			a.think = CSBot_BuyStart_Shop;
			a.nextthink = time + random(0, autocvar_mp_freezetime);
		}
	}
}

void
CSBot_RoundStart(void)
{
	/* if (g_cs_bombzones <= 0) {
		return;
	}
		for (entity a = world; (a = find(a, classname, "player"));) {
			if (clienttype(a) != CLIENTTYPE_REAL) {
				csbot targ;
				targ = (csbot)a;

				if (targ.team == TEAM_T)
					continue;
				if (targ.health <= 0)
					continue;
				
				targ.RunToRandomBombsite();
			}
		}
	} */
}

void
CSBot_RestartRound(void)
{
	// Reset some variables for all bots
	for (entity a = world; (a = find(a, classname, "player"));) {
		if (clienttype(a) != CLIENTTYPE_REAL) {
			csbot targ;
			targ = (csbot)a;

			if (targ.team == TEAM_T) {
				targ.m_actionIsPlanting = FALSE;
			}
			else {
				targ.m_actionIsDefusing = FALSE;
			}
		}
	}
}
